package db

import (
	"context"
	"errors"
	"runtime"
	"sync"
	"time"

	"engine/eng_cfg"
	"gitee.com/ameise84/e3lock"
	"gitee.com/ameise84/e3utils/uuid"
	"github.com/redis/go-redis/v9"
	"github.com/spf13/viper"
)

var keyTTL = 36 * time.Hour

type client struct {
	ctx  context.Context
	cli  redis.UniversalClient
	dump *dumper
}

func (ts *client) start() (err error) {
	isCluster := viper.GetBool(eng_cfg.RedisIsCluster)
	if isCluster {
		ts.startCluster(viper.GetStringSlice(eng_cfg.RedisAddr), viper.GetString(eng_cfg.RedisUser), viper.GetString(eng_cfg.RedisPassword))
	} else {
		addr := viper.GetStringSlice(eng_cfg.RedisAddr)
		ts.startClient(addr[0], viper.GetString(eng_cfg.RedisUser), viper.GetString(eng_cfg.RedisPassword))
	}
	r := ts.cli.Ping(context.Background())
	err = r.Err()
	if err != nil {
		_gDBLogger.Error("redis ping").Err(err).Println()
		return err
	}
	ts.dump = newDumper()
	err = ts.dump.start(viper.GetString(eng_cfg.RedisDumpDB), ts.cli)
	if err != nil {
		return err
	}
	ts.ctx = context.Background()
	return nil
}

func (ts *client) stop() {
	ts.dump.stop()
	_ = ts.cli.Close()
}

func (ts *client) startCluster(addr []string, usr, psw string) {
	ts.cli = redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:           addr,
		Username:        usr,
		Password:        psw,
		MaxIdleConns:    2 * runtime.NumCPU(),
		ConnMaxIdleTime: 2 * time.Minute,
		ReadTimeout:     5 * time.Second,
	})
}

func (ts *client) startClient(addr string, usr, psw string) {
	ts.cli = redis.NewClient(&redis.Options{
		Addr:            addr,
		Username:        usr,
		Password:        psw,
		MaxIdleConns:    2 * runtime.NumCPU(),
		ConnMaxIdleTime: 2 * time.Minute, //连接的最大空闲时间
		ReadTimeout:     5 * time.Second,
	})
}

func (ts *client) BaseCli() redis.UniversalClient {
	return nil
}

func (ts *client) Locker(key string) sync.Locker {
	return e3lock.NewRedisLock(ts.cli, key, uuid.New())
}

func (ts *client) LockDo(key string, fn func()) {
	l := e3lock.NewRedisLock(ts.cli, key, uuid.New())
	l.Lock()
	defer l.Unlock()
	fn()
}

func (ts *client) HDel(delDB bool, m TableMeta, ids ...string) error {
	key := m.TableName()
	r := ts.cli.HDel(ts.ctx, key, ids...)
	if delDB {
		ts.dump.Del(m.BaseTableName(), key, ids...)
	}
	return r.Err()
}

func (ts *client) HGet(m TableMeta, id string) error {
	key := m.TableName()
	r := ts.cli.HGet(ts.ctx, key, id)
	if r.Err() == nil {
		return r.Scan(m)
	}
	if errors.Is(r.Err(), redis.Nil) {
		mu := ts.Locker(key + id)
		mu.Lock()
		defer mu.Unlock()
		r = ts.cli.HGet(ts.ctx, key, id)
		if r.Err() == nil {
			return r.Scan(m)
		}
		err := ts.dump.Load(m, id)
		if err != nil {
			return err
		}
		return ts.hSet(key, id, m, keyTTL)
	}
	return nil
}

func (ts *client) HSet(m TableMeta) error {
	key := m.TableName()
	id := m.ID()
	if id == "" {
		panic("hest id is empty")
	}
	if err := ts.hSet(key, id, m, keyTTL); err != nil {
		return err
	}
	ts.dump.Save(m.BaseTableName(), key, id)
	return nil
}

func (ts *client) HSetEx(m TableMeta, ex time.Duration) error {
	key := m.TableName()
	id := m.ID()
	if id == "" {
		panic("hest id is empty")
	}
	if err := ts.hSet(key, id, m, ex); err != nil {
		return err
	}
	ts.dump.Save(m.BaseTableName(), key, id)
	return nil
}

func (ts *client) hSet(key, field string, m TableMeta, ex time.Duration) error {
	p := ts.cli.Pipeline()
	p.HSet(ts.ctx, key, field, m)
	if ex > 0 {
		p.HExpire(ts.ctx, key, ex, field)
	}
	pr, pe := p.Exec(ts.ctx)
	if pe != nil {
		return pe
	}
	for _, rs := range pr {
		if err := rs.Err(); err != nil {
			return err
		}
	}
	return nil
}
